using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using Inventory.BusinessEntities;
using Locator.LcboServices.LcboInventoryService.Services;
using Microsoft.Practices.CompositeWeb;
using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;
using Microsoft.Practices.EnterpriseLibrary.Logging;
using Locator.LcboServices.LcboStoresService.Services;
using DataAccess;
using Microsoft.Practices.ObjectBuilder;
using DataAccess.BusinessEntities;
using Locator.LcboServices.BusinessEntities;
using DataAccess.Services;

namespace Locator.Inventory
{
    public class InventoryController : IInventoryController
    {
        private bool disposed;
        private readonly TraceManager traceManager;
        private readonly LogWriter logWriter;
        private readonly ILcboInventoryService lcboService;
        private readonly ILcboStoresService lcboStoresService;
        private readonly IDomainContext domainContext;

        public InventoryController
            (
            [ServiceDependency] ILcboInventoryService lcboService,
            [ServiceDependency] ILcboStoresService lcboStoresService,
            [CreateNew] IDomainContext domainContext
            )
        {
            this.lcboService = lcboService;
            this.lcboStoresService = lcboStoresService;
            this.logWriter = EnterpriseLibraryContainer.Current.GetInstance<LogWriter>();
            this.traceManager = new TraceManager(this.logWriter);
            this.domainContext = domainContext;
        }

        public List<StoreWithInventory> GetInventoriesWithRequest(InventoryRequest request)
        {
            List<StoreWithInventory> result = new List<StoreWithInventory>();
            // The result dictionary - key is the store ID, value is the store
            Dictionary<int, StoreWithInventory> resultStores = new Dictionary<int, StoreWithInventory>();

            using (traceManager.StartTrace("General"))
            {
                var userEntity = this.GetUserEntity();
                var productsToFind = request.GetProductIdsToFind(userEntity);

                // Get the results from the LCBO website
                List<LcboInventoryData2> lcboInventory = this.lcboService.RetrieveMatches(productsToFind);

                // Detect new and changed stores
                this.DetectNewAndChangedStores(lcboService, lcboInventory);

                // Get the store Ids
                var storeids = request.GetStoreNumbersToFind(this.domainContext, userEntity);

                //TODO: filter by store/product selection

                Dictionary<int, ProductListItem> productDictionary = new Dictionary<int, ProductListItem>();

                // The LCBO results filtered by the stores in the request
                var lcboInventoriesFiltered = from l in lcboInventory where storeids.Contains(l.StoreNumber) select l;

                foreach (var lcboInventoryItem in lcboInventoriesFiltered)
                {
                    StoreWithInventory currentStoreWithInventory;
                    if (resultStores.ContainsKey(lcboInventoryItem.StoreNumber))
                    {
                        // The current store is in the result dictionary, so set currentStore to it.
                        currentStoreWithInventory = resultStores[lcboInventoryItem.StoreNumber];
                    }
                    else
                    {
                        // The current store is not in the result dictionary.
                        // Set currentStore to be a new store
                        var Store = this.domainContext.Stores.Single(s => s.Id == lcboInventoryItem.StoreNumber);
                        var store = new InvStore()
                        {
                            Address = Store.Address,
                            City = Store.City,
                            Hours = Store.Hours,
                            Latitude = Store.Latitude,
                            Longitude = Store.Longitude,
                            StoreNumber = Store.Id
                        };

                        currentStoreWithInventory = new StoreWithInventory() { Store = store };
                        resultStores.Add(lcboInventoryItem.StoreNumber, currentStoreWithInventory);
                    }

                    // Inventory.CreateInventory(lcboInventoryItem.StoreNumber, lcboInventoryItem.ProductId);
                    // Set its level to that of the lcbo result value
                    ProductListItem productsVm = null;
                    if (productDictionary.ContainsKey(lcboInventoryItem.ProductId))
                    {
                        // Current product ID is in the dictionary, so set currentProduct to it
                        productsVm = productDictionary[lcboInventoryItem.ProductId];
                    }
                    else
                    {
                        // Current product ID is not in the dictionary
                        var product = this.domainContext.Products.Where(p => p.Id == lcboInventoryItem.ProductId).Include("Substance").Include("Container").First();
                        productsVm = new ProductListItem() { Name = product.Name };
                        productDictionary.Add(lcboInventoryItem.ProductId, productsVm);
                    }

                    // Create an inventory item
                    InventoryInfo currentInventory = new InventoryInfo
                    {
                        NumberInStock = lcboInventoryItem.NumberInStock,
                        Product = productsVm
                    };

                    // Set the current inventory's product to a new product based on the current product
                    // Attach the inventory item to the current store.  So we have Store.Inventory.Product and Store.Inventory.InventoryLevel.
                    currentStoreWithInventory.Inventory.Add(currentInventory);
                }

            }

            result = new List<StoreWithInventory>(resultStores.Values);

            return result;
        }

        private void DetectNewAndChangedStores(ILcboInventoryService lcboService, List<LcboInventoryData2> lcboInventory)
        {
            var newStoreNumbers = new List<int>();
            //var changedStoreNumbers = new List<int>();
            //TODO: Use a dictionary of stores to minimize the number of calls to Stores.FirstOrDefault
            lcboInventory.ForEach(inventoryItem =>
            {
                Store aStoreStore = this.domainContext.Stores.FirstOrDefault(s => s.Id == inventoryItem.StoreNumber);
                //if (aStoreStore != null)
                //{
                //    if (!aStoreStore.City.Equals(inventoryItem.City, StringComparison.OrdinalIgnoreCase) || !aStoreStore.Address.Equals(inventoryItem.StreetAddress, StringComparison.OrdinalIgnoreCase))
                //    {
                //        // A store is considered changed if the city or address from the LCBO website is different from what's in the database
                //        if (!changedStoreNumbers.Contains(inventoryItem.StoreNumber))
                //        {
                //            changedStoreNumbers.Add(inventoryItem.StoreNumber);
                //            this.logWriter.Write(String.Format(CultureInfo.CurrentCulture, "Found changed address {0} {1}, store number {2}", inventoryItem.StreetAddress, inventoryItem.City, inventoryItem.StoreNumber));
                //        }
                //    }
                //}
                //else
                //{
                if (aStoreStore == null && !newStoreNumbers.Contains(inventoryItem.StoreNumber))
                {
                    // A store is considered new if the store number from the LCBO website is not in the database
                    newStoreNumbers.Add(inventoryItem.StoreNumber);
                    this.logWriter.Write(String.Format(CultureInfo.CurrentCulture, "Found new store at address {0} {1}. store number {2}", inventoryItem.StreetAddress, inventoryItem.City, inventoryItem.StoreNumber));
                }
                //}
            });

            //this.logWriter.Write("Built changed or new store IDs after {0} milliseconds", operationStopwatch.ElapsedMilliseconds);
            // Look up new stores and add to context
            newStoreNumbers.ForEach(newStoreNumber =>
            {
                // Get store info from LCBO and add to DB
                // TODO: multi-thread the requests to get all new and changed store details
                LcboStoreInformation st = lcboStoresService.GetLocationDetails(newStoreNumber);
                Store newStore = new Store() { Id = newStoreNumber };
                newStore.Address = st.AddressLine;
                newStore.City = st.City;
                newStore.Latitude = st.Latitude;
                newStore.Longitude = st.Longitude;
                this.domainContext.Save(newStore);
            });

            //this.logWriter.Write("Created new stores after {0} milliseconds", operationStopwatch.ElapsedMilliseconds);
            // Look up changed stores and apply changes to context
            //changedStoreNumbers.ForEach(changedStoreNumber =>
            //{
            //    LcboStoreInformation changedStore = lcboStoresService.GetLocationDetails(changedStoreNumber);
            //    Store storeEntity = this.unitOfWork.Stores.FindById(changedStoreNumber);
            //    storeEntity.Address = changedStore.AddressLine;
            //    storeEntity.City = changedStore.City;
            //    storeEntity.Latitude = changedStore.Latitude;
            //    storeEntity.Longitude = changedStore.Longitude;
            //});

            //Trace.TraceInformation("Updated changed stores after {0} milliseconds", operationStopwatch.ElapsedMilliseconds);
            this.domainContext.SaveChanges();
            //Trace.TraceInformation("Saved store changes after {0} milliseconds", operationStopwatch.ElapsedMilliseconds);
        }

        private User GetUserEntity()
        {
            string userName = Thread.CurrentPrincipal.Identity.Name;
            User result = null;

            var userEntities = this.domainContext.Users.Where(u => String.Compare(u.Username, userName, true) == 0)
                .Include(u => u.Stores)
                .Include(u => u.Products)
                .Include(u => u.Products.Select(p => p.Container))
                .Include(u => u.Products.Select(p => p.Substance))
                ;
            if (userEntities.Count() == 0)
            {
                User newUser = new User() { Username = userName };
                this.domainContext.Save(newUser);
                this.domainContext.SaveChanges();
                newUser = this.domainContext.Users.Where(u => String.Compare(u.Username, userName, true) == 0)
                    .Include(u => u.Stores)
                    .Include(u => u.Products)
                    .Include(u => u.Products.Select(p => p.Container))
                    .Include(u => u.Products.Select(p => p.Substance))
                    .First();
                result = newUser;
                //                throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, "Couldn't find a user with user name {0} and ID {1}", userName, guid));
            }
            else
            {
                result = userEntities.First();
            }

            return result;
        }

        public List<Store> GetStoresForCurrentUser()
        {
            List<Store> result = new List<Store>();
            using (traceManager.StartTrace("General"))
            {
                var userEntity = this.GetUserEntity();
                result.AddRange(userEntity.Stores);
            }

            return result;
        }

        public List<ProductListItem> GetProductsForCurrentUser()
        {
            List<ProductListItem> result = new List<ProductListItem>();
            using (traceManager.StartTrace("General"))
            {
                var userEntity = this.GetUserEntity();
                foreach (var product in userEntity.Products)
                {
                    result.Add(new ProductListItem() { Id = product.Id, Name = product.Name });
                }
            }

            return result;
        }

        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!this.disposed)
            {
                if (disposing)
                {
                    if (this.logWriter != null)
                    {
                        this.logWriter.Dispose();
                    }

                    if (this.lcboService != null)
                    {
                        this.lcboService.Dispose();
                    }

                    if (this.lcboStoresService != null)
                    {
                        this.lcboStoresService.Dispose();
                    }

                    if (this.domainContext != null)
                    {
                        this.domainContext.Dispose();
                    }
                }
            }

            this.disposed = true;
        }

        ~InventoryController()
        {
            this.Dispose(false);
        }
    }
}
